Skip to content

Harden CI gates, ingestion commits, and SECURITY DEFINER grants#696

Merged
BigSimmo merged 2 commits into
mainfrom
claude/repo-hardening
Jul 17, 2026
Merged

Harden CI gates, ingestion commits, and SECURITY DEFINER grants#696
BigSimmo merged 2 commits into
mainfrom
claude/repo-hardening

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Re-land of the three highest-risk audit findings on current main. Supersedes #694, which was based on a now-95-commit-stale base (main has since independently fixed the worker commit-error path). Folds in the CodeRabbit review from #694.

CI gate integrity

  • The offline RAG grounding preflight (eval:rag:offline) now runs for every non-docs change in both CI and local verify:pr-local — previously gated on the rag_eval_changed allowlist, so a new retrieval file outside the patterns could silently skip a clinical-safety gate.
  • Added the check:type-scale, check:icon-scale, brand:check, and check:function-grants guards to the always-run static-pr job (previously local-only in verify:cheap, with no CI or test backstop).
  • Added a ci-change-scope self-test for the scope-narrowing safety net.

Ingestion data-safety (worker/main.ts)

  • Refuse to commit an empty index generation (0 chunks + 0 searchable images), so an atomic reindex can no longer swap a previously-good index for nothing (e.g. an image-only PDF OCR could not read). The job fails and the prior generation stays live. (Harden CI gates, ingestion commits, and SECURITY DEFINER grants #694's #3b is intentionally dropped — main already fails closed on commit errors.)

SECURITY DEFINER grants

  • Added scripts/check-function-grants.mjs (+ check:function-grants, wired into static-pr and verify:cheap, with tests/function-grants.test.ts). It fails if a SECURITY DEFINER public function is executable by PUBLIC/anon. Incorporates the CodeRabbit review: a revoke only protects when its FROM list includes PUBLIC (a revoke from anon only is not enough), and both named and schema-wide GRANT … TO public/anon are detected. Current schema passes (all 20 SECURITY DEFINER functions revoked); overload-by-exact-signature matching is a noted follow-up in the script header.

Verification

Run locally and offline — no OpenAI/Supabase/live-DB/hosted-CI calls, per the provider-confirmation boundary.

Passed:

  • check:ci-scope (self-test incl. the new case), check:function-grants, check:type-scale, check:icon-scale, brand:check
  • tests/function-grants.test.ts — 9/9 (incl. anon-only-revoke and schema-wide-grant regressions)
  • 63 worker/ingestion/reindex vitest tests
  • verify:pr-local --dry-run confirms the offline RAG gate now lists for a non-docs RAG lib file
  • lint clean on all changed files; prettier --check clean

Not run (and why):

  • npm run verify:pr-local (full) — its build/client-bundle scan and full typecheck need devDeps not installed in this container (CI's npm ci installs them). Offline constituents run individually.
  • npm run check:production-readiness — not run, to respect the provider boundary; recommend running in CI given the ingestion surface.

Clinical Governance Preflight

Change touches ingestion (worker commit path) and CI/deployment config.

  • Source-backed claims still require linked source verification before clinical use — unchanged
  • No patient-identifiable document workflow was introduced or expanded without explicit governance approval
  • Supabase target remains Clinical KB Database — unchanged
  • Service-role keys and private document access remain server-only — unchanged; the function-grant guard reinforces this
  • Demo/synthetic content remains clearly separated from real clinical sources — unchanged
  • Source metadata, review status, and outdated/unknown-source behavior remain conservative — the empty-commit gate makes ingestion strictly more conservative
  • Deployment classification/TGA SaMD impact was checked — no change to clinical decision-support behavior; safety guards only

Notes

  • Scoped to the top-3 audit findings. Remaining findings (config defaults, pagination-500, log redaction, perf, a11y, doc/structure staleness, RAG deadline-composition) are deferred to follow-up PRs.
  • Guard limitation (documented): overloaded functions are matched by bare name; exact per-signature matching is a follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_019UQUcNriJUVSGRhWz794RQ


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented empty document extractions from replacing valid search indexes.
    • Failed ingestion jobs now follow standard retry and error handling when no searchable content is produced.
  • Safety & Validation

    • Added automated checks to prevent unauthorized access to security-sensitive database functions.
    • Offline retrieval validation now runs for all non-documentation changes, improving coverage of safety checks.

Re-land of the top-3 audit fixes on current main (supersedes #694, which was
based on a now-95-commit-stale base). Folds in the CodeRabbit review from #694.

CI gate integrity:
- Offline RAG grounding preflight runs for every non-docs change in BOTH CI and
  local verify:pr-local (previously gated on the rag_eval_changed allowlist, so a
  new retrieval file could silently skip a clinical-safety gate).
- Added the type-scale, icon-scale, brand, and function-grant guards to the
  always-run static-pr job (previously local-only, no CI/test backstop).
- Added a ci-change-scope self-test for the scope-narrowing safety net.

Ingestion data-safety (worker/main.ts):
- Refuse to commit an empty index generation (0 chunks + 0 searchable images) so
  an atomic reindex can no longer swap a previously-good index for nothing. (#3b
  from #694 is dropped: main already fails closed on commit errors.)

SECURITY DEFINER grants:
- Added scripts/check-function-grants.mjs (+ check:function-grants, wired into
  static-pr and verify:cheap, with tests). Fails if a SECURITY DEFINER public
  function is executable by PUBLIC/anon. Incorporates the CodeRabbit review: a
  revoke only protects when its FROM list includes PUBLIC (revoke-from-anon is not
  enough), and named + schema-wide GRANTs to PUBLIC/anon are detected. Current
  schema passes (20 functions); overload-by-exact-signature is a noted follow-up.

Verified offline: check:ci-scope, check:function-grants, check:type-scale,
check:icon-scale, brand:check, function-grants test (9/9), 63 worker/ingestion
tests, verify:pr-local dry-run, lint clean on all changed files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UQUcNriJUVSGRhWz794RQ
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a security-definer function privilege checker and CI wiring, runs offline RAG evaluation for all non-docs changes, tests expanded scope classification, and prevents workers from committing generations with neither chunks nor searchable images.

Changes

Function grant enforcement

Layer / File(s) Summary
Security-definer privilege checker
scripts/check-function-grants.mjs
Parses schema functions and privilege statements, then fails for security-definer functions executable by public or anonymous callers.
Grant-checker wiring and coverage
package.json, .github/workflows/ci.yml, tests/function-grants.test.ts
Adds the npm command, runs it in static CI guards, and tests valid, invalid, and ordering-sensitive SQL fixtures.

Offline RAG evaluation gating

Layer / File(s) Summary
Advisory RAG scope classification
scripts/ci-change-scope.mjs
Documents rag_eval_changed as advisory and tests classification of an out-of-allowlist RAG library change.
Non-docs offline evaluation wiring
.github/workflows/ci.yml, scripts/verify-pr-local.mjs
Runs offline RAG evaluation for non-docs-only changes and skips it only for docs-only changes.

Empty index commit protection

Layer / File(s) Summary
Pre-commit empty-output validation
worker/main.ts
Throws before index commit when processing produces zero chunks and zero searchable images.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: CI gate hardening, ingestion commit safety, and SECURITY DEFINER grant validation.
Description check ✅ Passed The description follows the required template and includes summary, verification, clinical governance preflight, and notes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/repo-hardening

Comment @coderabbitai help to get the list of available commands.

@supabase

supabase Bot commented Jul 17, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@BigSimmo
BigSimmo enabled auto-merge July 17, 2026 05:44
@BigSimmo
BigSimmo merged commit 0ebd03c into main Jul 17, 2026
18 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75f2ebae07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +115
const grantNamedRe =
/grant\s+(?:execute|all(?:\s+privileges)?)\s+on\s+function\s+(?:public\.)?"?([a-z0-9_]+)"?[^;]*?\bto\b[^;]*?\b(?:public|anon)\b/gi;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject grants to authenticated users

When a migration revokes a SECURITY DEFINER RPC and later grants EXECUTE to authenticated, this regex does not record the reopening, so check:function-grants reports the function safe. An authenticated user can invoke a SECURITY DEFINER function directly, bypassing RLS; for any such RPC lacking its own caller/tenant enforcement, that recreates the cross-tenant access surface this gate is intended to block. Extend the role match to include authenticated (and add a regression fixture).

Useful? React with 👍 / 👎.

@BigSimmo
BigSimmo deleted the claude/repo-hardening branch July 17, 2026 05:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/check-function-grants.mjs`:
- Around line 66-68: Update the schema-wide grant detection around
schemaWideGrantRe and its lines.some check to evaluate complete SQL statements
rather than individual physical lines, allowing whitespace and newlines between
SQL tokens. Ensure multiline GRANT ... ON ALL FUNCTIONS ... TO public or anon
statements are detected, and add a fixture covering a multiline grant.
- Around line 114-118: The grant parsing around grantNamedRe must capture every
function listed after ON FUNCTION, not only the first routine, and add each
routine name to grantedToAnon. Update the parsing logic to handle
comma-separated, optionally schema-qualified and argument-bearing function
specifications, then add a regression fixture where a non-definer function
appears first and a previously revoked function receives the anon grant.
- Around line 104-131: Update the ACL parsing around revokeRe and grantNamedRe
to retain each mutation’s statement position and target roles, then evaluate
mutations in SQL order so later REVOKE statements remove earlier PUBLIC/anon
grants from effective access. Use the final effective ACL when deciding
vulnerability in the byName loop, and add a regression test covering GRANT
followed by REVOKE for public and anon.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bc348aeb-5933-4367-97cd-d71a737d9aae

📥 Commits

Reviewing files that changed from the base of the PR and between 8b1a415 and 75f2eba.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • package.json
  • scripts/check-function-grants.mjs
  • scripts/ci-change-scope.mjs
  • scripts/verify-pr-local.mjs
  • tests/function-grants.test.ts
  • worker/main.ts

Comment on lines +66 to +68
const schemaWideGrantRe =
/grant\s+(?:execute|all(?:\s+privileges)?)\s+on\s+all\s+functions\s+in\s+schema\s+public\s+to\s+[^;]*?\b(?:public|anon)\b/i;
if (lines.some((line) => schemaWideGrantRe.test(line))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Parse schema-wide grants as complete SQL statements.

Testing one physical line misses valid multiline GRANT ... ON ALL FUNCTIONS ... TO anon statements. Such a grant can re-open every SECURITY DEFINER function while this guard reports success. Parse statement-level SQL with whitespace-flexible matching and add a multiline-grant fixture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-function-grants.mjs` around lines 66 - 68, Update the
schema-wide grant detection around schemaWideGrantRe and its lines.some check to
evaluate complete SQL statements rather than individual physical lines, allowing
whitespace and newlines between SQL tokens. Ensure multiline GRANT ... ON ALL
FUNCTIONS ... TO public or anon statements are detected, and add a fixture
covering a multiline grant.

Comment on lines +104 to +131
const revokeRe =
/revoke\s+(?:execute|all(?:\s+privileges)?)\s+on\s+function\s+(?:public\.)?"?([a-z0-9_]+)"?[^;]*?\bfrom\b([^;]*)/gi;
const revoked = new Set();
let rm;
while ((rm = revokeRe.exec(sql))) {
if (/\bpublic\b/i.test(rm[2])) revoked.add(rm[1].toLowerCase());
}

// A named GRANT of EXECUTE to PUBLIC/anon re-opens that function even after a
// revoke — this is what a migration adding an anon-callable RPC looks like.
const grantNamedRe =
/grant\s+(?:execute|all(?:\s+privileges)?)\s+on\s+function\s+(?:public\.)?"?([a-z0-9_]+)"?[^;]*?\bto\b[^;]*?\b(?:public|anon)\b/gi;
const grantedToAnon = new Set();
let gm;
while ((gm = grantNamedRe.exec(sql))) grantedToAnon.add(gm[1].toLowerCase());

let definerCount = 0;
const vulnerable = [];
for (const [name, info] of byName) {
if (!info.isDefiner) continue;
definerCount += 1;
if (ALLOWLIST.has(name)) continue;
if (grantedToAnon.has(name)) {
vulnerable.push({ name, idx: info.earliestIdx, reason: "explicitly grants EXECUTE to PUBLIC/anon" });
continue;
}
if (coveredByBlanket(info.earliestIdx)) continue;
if (revoked.has(name)) continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Evaluate ACL mutations in statement order.

revoked and grantedToAnon discard positions, so a grant followed by REVOKE ... FROM public, anon, authenticated is still reported as vulnerable even though the final ACL is safe. Retain statement order and role targets when computing effective access; add the inverse ordering test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-function-grants.mjs` around lines 104 - 131, Update the ACL
parsing around revokeRe and grantNamedRe to retain each mutation’s statement
position and target roles, then evaluate mutations in SQL order so later REVOKE
statements remove earlier PUBLIC/anon grants from effective access. Use the
final effective ACL when deciding vulnerability in the byName loop, and add a
regression test covering GRANT followed by REVOKE for public and anon.

Comment on lines +114 to +118
const grantNamedRe =
/grant\s+(?:execute|all(?:\s+privileges)?)\s+on\s+function\s+(?:public\.)?"?([a-z0-9_]+)"?[^;]*?\bto\b[^;]*?\b(?:public|anon)\b/gi;
const grantedToAnon = new Set();
let gm;
while ((gm = grantNamedRe.exec(sql))) grantedToAnon.add(gm[1].toLowerCase());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Inspect every function in a multi-function GRANT.

The regex captures only the first routine after ON FUNCTION. For example, a grant to public.plain(), public.sensitive(uuid) records only plain; if sensitive was previously revoked, the checker can pass despite its new anon grant. Parse all routine specifications and add a regression fixture with a non-definer first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-function-grants.mjs` around lines 114 - 118, The grant parsing
around grantNamedRe must capture every function listed after ON FUNCTION, not
only the first routine, and add each routine name to grantedToAnon. Update the
parsing logic to handle comma-separated, optionally schema-qualified and
argument-bearing function specifications, then add a regression fixture where a
non-definer function appears first and a previously revoked function receives
the anon grant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants